1 module serve; 2 3 import arsd.cgi; 4 import std.stdio; 5 import std.file; 6 import std.string; 7 import std.process; 8 9 // -Wl,--export=__heap_base 10 11 // https://github.com/skoppe/wasm-sourcemaps 12 13 enum DEFAULT_PATH="build"; 14 private __gshared string startPath; 15 16 string extendedContentTypeFromFileExtension(string thing) 17 { 18 string ret = contentTypeFromFileExtension(thing); 19 if(ret != null) 20 return ret; 21 22 if(ret.endsWith(".ogg")) 23 return "application/ogg"; 24 if(ret.endsWith(".mp4")) 25 return "application/mp4"; 26 27 return "application/octet-stream"; 28 } 29 30 import core.thread; 31 import core.sync.mutex; 32 import core.sync.semaphore; 33 void pushWebsocketMessage(string message) 34 { 35 synchronized 36 { 37 foreach(ref conn; connections) 38 conn.messages~= message; 39 } 40 // socket.send(message); 41 // synchronized websocketMessages~= message; 42 } 43 44 struct Connection 45 { 46 size_t id; 47 string[] messages; 48 } 49 private __gshared size_t id; 50 private __gshared Connection*[] connections; 51 52 void serveGameFiles(Cgi cgi) 53 { 54 import std.path; 55 string targetPath = buildNormalizedPath(startPath, DEFAULT_PATH); 56 string contentType = extendedContentTypeFromFileExtension(cgi.pathInfo); 57 string file = buildNormalizedPath(targetPath, cgi.pathInfo[1..$]); 58 59 if(cgi.websocketRequested()) 60 { 61 auto socket = cgi.acceptWebsocket(); 62 Connection conn = Connection(id); 63 synchronized 64 { 65 connections~= &conn; 66 id++; 67 } 68 enum __updateMsg = 0xff; 69 auto msg = socket.recv(); 70 while(msg.opcode != WebSocketOpcode.close) 71 { 72 synchronized 73 { 74 foreach(socketMsg; conn.messages) 75 socket.send(socketMsg); 76 conn.messages.length = 0; 77 } 78 msg = socket.recv(); 79 } 80 81 synchronized 82 { 83 socket.close(); 84 import std.algorithm; 85 ptrdiff_t index = countUntil!((Connection* c) => c.id == conn.id)(connections); 86 if(index != -1) 87 { 88 connections = connections[0..index]~ connections[index+1..$]; 89 } 90 } 91 writeln("AutoReloading WebSocket[", conn.id, "] closed."); 92 } 93 else if(cgi.pathInfo == "/") 94 { 95 string indexHTML = buildNormalizedPath(targetPath, "index.html"); 96 if(exists(indexHTML)) 97 { 98 import std.string; 99 string reloadServer = replace(import("reload_server.js"), "$WEBSOCKET_SERVER$", "ws://localhost:"~server.listeningPort.to!string); 100 cgi.write(readText(indexHTML)~"<script> "~reloadServer~"</script>", true); 101 } 102 else 103 { 104 // index 105 string html = "<html><head><title>Hipreme Engine Webassembly Server</title></head><body><ul>"; 106 foreach(string name; dirEntries(targetPath, SpanMode.shallow)) 107 { 108 name = name[targetPath.length..$]; 109 html ~= "<li><a href=\"" ~ name ~ "\">" ~ name ~"</a></li>"; 110 } 111 html~= "</body></html>"; 112 cgi.write(html, true); 113 114 } 115 } 116 else if(contentType) 117 { 118 if(!exists(file)) 119 { 120 cgi.setResponseStatus("404 file not found"); 121 } 122 else 123 { 124 cgi.setResponseHeader("Access-Control-Expose-Headers: Content-Length"); 125 cgi.setResponseContentType(contentType); 126 cgi.setResponseStatus(200); 127 if(contentType[0.."text".length] == "text") 128 cgi.write(readText(file), true); 129 else 130 cgi.write(read(file), true); 131 writeln("GET ", file, " 200"); 132 } 133 } 134 135 } 136 137 private RequestServer server; 138 /++ 139 This is the function [GenericMain] calls. View its source for some simple boilerplate you can copy/paste and modify, or you can call it yourself from your `main`. 140 Params: 141 fun = Your request handler 142 CustomCgi = a subclass of Cgi, if you wise to customize it further 143 maxContentLength = max POST size you want to allow 144 args = command-line arguments 145 History: 146 Documented Sept 26, 2020. 147 +/ 148 149 150 151 void hipengineCgiMain(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength) 152 (string[] args, string servePath, shared ushort* port, shared Semaphore sem) if(is(CustomCgi : Cgi)) 153 { 154 startPath = servePath; 155 if(tryAddonServers(args)) 156 return; 157 if(trySimulatedRequest!(fun, CustomCgi)(args)) 158 return; 159 160 // you can change the port here if you like 161 *port = 9000; 162 server.listeningPort = *port; 163 164 string host = server.listeningHost; 165 if(host == "") host = "localhost"; 166 167 168 // then call this to let the command line args override your default 169 server.configureFromCommandLine(args); 170 171 172 173 // and serve the request(s). 174 175 while(*port != 0) 176 { 177 try{ 178 server.listeningPort = *port; 179 writeln("HipremeEngine Dev Server listening from ", host,":",server.listeningPort); 180 (cast()sem).notify; 181 server.serve!(fun, CustomCgi, maxContentLength)(); 182 return; 183 } 184 catch(Exception e) 185 { 186 *port = cast(ushort)(*port + 1); 187 } 188 } 189 190 writeln("Hipreme Engine Dev server could not start."); 191 } 192 193 194 void stopServer() 195 { 196 import core.stdc.stdlib; 197 pushWebsocketMessage("close"); 198 exit(0); 199 }